home *** CD-ROM | disk | FTP | other *** search
/ Developer CD Series 1997 January: Mac OS SDK / Dev.CD Jan 97 SDK2.toast / Development Kits (Disc 2) / OpenDoc Development Framework / Documentation / Development Notes / ODF vs Native Exceptions & RTTI < prev    next >
Encoding:
Text File  |  1996-09-19  |  19.7 KB  |  299 lines  |  [TEXT/ttxt]

  1. OpenDoc
  2. Development
  3. Framework
  4.                                                                                                                                                                                      
  5. ODF vs Native Exceptions & RTTI 
  6. ODF Release 2                                                                                                                                                            
  7.  
  8. This document discusses the issues relating to exception handling and RTTI support in ODF.
  9.  
  10.  
  11. Important Note: It is important to note that, starting with ODF Release 2, the ODF RTTI and Emulated Exceptions subsystems are used only for MPW based compilers (Symantec C++ 68K and MRC). Native RTTI and Exceptions are now used for both CodeWarrior 68K and PPC.
  12.  
  13.  
  14. Table of Contents
  15. -------------------------
  16. • Overview
  17. • Quick Review of ODF Exceptions and RTTI Emulation
  18. • Pros and Cons
  19. • Using Native Exceptions
  20. • Using ODF Emulated Exceptions
  21.     • Theory of Operation
  22.     • Limitations
  23.  
  24.  
  25.  
  26. OverView
  27.  
  28. Two relatively recent additions to the C++ Language are exception handling and runtime type identification (RTTI).  Not all C++ compilers fully support exceptions and RTTI, but ODF was designed with the assumption that these two features of the language are available.  ODF therefore provides subsets of these features in emulation for compilers that haven't yet implemented them.
  29.  
  30. ODF and ODF parts can be built using either native or emulated exception handling and RTTI.  By "native", we mean that ODF will use the C++ syntax defined in the proposed standard for ANSI/ISO C++, and rely on the compiler (and the compiler's runtime library) to implement the correct semantics.  By "emulated", we mean that ODF will use its own runtime library.  Both approaches can be used with the same code base by using the macros defined in FWExcDef.h (for exceptions) and FWClaInf.h (for RTTI).  If you use these macros (as ODF does throughout), you can easily switch from emulated exceptions and RTTI to native exceptions and RTTI (and back again).  There are pros and cons with either approach (native vs emulated).  We discuss these pros and cons here, so that you can decide which approach is best for you.
  31.  
  32.  
  33.  
  34. Quick Review of ODF Exceptions and RTTI Emulation
  35.  
  36. Let's start with a simple example using ODF RTTI emulation.  Suppose we have three classes related by inheritance like this:
  37.  
  38. CMyBase
  39.     CMyDerived1
  40.     CMyDerived2
  41.  
  42. We might want to use RTTI to see if a CMyBase* points to a CMyDerived1.  Using C++ RTTI, we'd do it like this:
  43.  
  44. void foo(CMyBase* object)
  45. {
  46.     CMyDerived1* derived = dynamic_cast<CMyDerived*> object;
  47.     if (derived != NULL)
  48.     {
  49.         // object is an instance of CMyDerived1
  50.     }
  51. }
  52.  
  53. With ODF RTTI, we'd have to use the FW_DYNAMIC_CAST macro instead of using the dynamic_cast operator, so the code would look like this:
  54.  
  55. void foo(CMyBase* object)
  56. {
  57.     CMyDerived1* derived = FW_DYNAMIC_CAST(CMyDerived, object);
  58.     if (derived != NULL)
  59.     {
  60.         // object is an instance of CMyDerived1
  61.     }
  62. }
  63.  
  64. This is pretty simple, but unfortunately it's not enough.  Since the compiler doesn't generate the RTTI information needed, we have to generate the information programmatically.  We do that via additional macros.  First, in the class declarations (inside the .h files), we use the FW_DECLARE_CLASS macro:
  65.  
  66. class CMyBase
  67. {
  68. public:
  69.     FW_DECLARE_CLASS
  70.     ... // other stuff as usual
  71. };
  72.  
  73. class CMyDerived1
  74. {
  75. public:
  76.     FW_DECLARE_CLASS
  77.     ... // other stuff as usual
  78. };
  79.  
  80. class CMyDerived2
  81. {
  82. public:
  83.     FW_DECLARE_CLASS
  84.     ... // other stuff as usual
  85. };
  86.  
  87. Also, where we define the class members (inside the .cpp files), we use the FW_DEFINE_CLASS_Mn (where "n" is the number of base classes) macros:
  88.  
  89. FW_DEFINE_CLASS_M0(CMyBase)
  90. FW_DEFINE_CLASS_M1(CMyDerived1, CMyBase)
  91. FW_DEFINE_CLASS_M1(CMyDerived2, CMyBase)
  92.  
  93. From this example we can see that there are two kinds of macros, known as translation macros and scaffolding macros.  FW_DYNAMIC_CAST is an example of a translation macro.  FW_DECLARE_CLASS is an example of a scaffolding macro.  When ODF is compiled for native exception handling and RTTI, the translation macros are defined to generate the underlying native syntax, whereas the scaffolding macros are defined to generate nothing.  When ODF is compiled for emulated exception handling and RTTI, the translation macros make calls to private interfaces in ODF's emulation subsystems.  The scaffolding macros generate code and data used by the emulation subsystem.
  94.  
  95. ODF RTTI defines these macros:
  96.  
  97. Translation Macros
  98. FW_DYNAMIC_CAST
  99. FW_TYPEID_FROM_TYPE
  100. FW_TYPEID_FROM_POINTER
  101.  
  102. Scaffolding Macros
  103. FW_DECLARE_CLASS
  104. FW_DEFINE_CLASS_M0 .. FW_DEFINE_CLASS_M4
  105.  
  106. ODF Exceptions defines these macros:
  107.  
  108. Translation Macros
  109. FW_TRY
  110. FW_CATCH
  111. FW_CATCH_REFERENCE
  112. FW_CATCH_NO_INSTANCE
  113. FW_CATCH_EVERYTHING
  114. FW_THROW
  115. FW_NEW
  116.  
  117. Scaffolding Macros
  118. FW_DECLARE_AUTO
  119. FW_DEFINE_AUTO
  120. FW_DEFINE_AUTO_TEMPLATE
  121. FW_DEFINE_AUTO_TEMPLATE2
  122. FW_END_CONSTRUCTOR
  123. FW_START_DESTRUCTOR
  124. FW_CATCH_BEGIN
  125. FW_CATCH_END
  126. FW_VOLATILE
  127. FW_DECLARE_EXCEPTION
  128. FW_DEFINE_EXCEPTION
  129.  
  130.  
  131.  
  132. Pros and Cons
  133.  
  134. Let's now consider the pros and cons of emulation vs. native exception handling and RTTI.  First, we'll note that exception handling is the harder problem, and therefore has a more significant set of pros and cons.  Also, ODF emulated exception handling requires ODF emulated RTTI, so you can't use native RTTI unless you also use native exception handling.  For these reasons, we'll focus the analysis of pros and cons on exception handling.
  135.  
  136. Native Exception Handling
  137.  
  138. Pros:
  139. 1) Defined by the proposed standard.
  140. 2) Typically results in faster code.
  141. 3) You can take advantage of the full specification for exception handling.
  142.  
  143. Cons:
  144. 1) Not all compilers implement exception handling.
  145. 2) Typically results in a larger code footprint.
  146. 3) Requires extra work to switch to emulated exceptions should you change your mind.
  147.  
  148.  
  149. ODF Emulated Exception Handling
  150.  
  151. Pros:
  152. 1) Usable with all C++ compilers, does not require any support from the compiler
  153. 2) Typically results in leaner code.
  154. 3) Easy to switch to native exceptions.
  155.  
  156. Cons:
  157. 1) Requires extra work by the programmer (FW_DECLARE_AUTO, FW_DEFINE_AUTO, FW_END_CONSTRUCTOR, FW_START_DESTRUCTOR).
  158. 2) Requires the use of ugly macros (FW_TRY, FW_CATCH_BEGIN, ... )
  159. 3) Typically results in slower code.
  160. 4) Has some restrictions from the full definition.
  161.  
  162. Basically, compilers that implement "zero runtime overhead" exceptions will generate code that is faster, but fatter, than code using ODF emulated exceptions.  The total code size of ODF parts is typically just under 10% bigger when using native exceptions.  If a 10% increase in code size is not a problem for your parts, we suggest you use native exceptions and RTTI.
  163.  
  164. Note that whichever choice you use, you must stick to that choice everywhere within your part editor, including all static libraries you link against, such as ODF itself.  If you build multiple editors with ODF, you can use native exceptions in some editors and emulated exceptions in other editors, but within each editor you must consistently use only one form of exception handling.  In particular, you must be sure to build the ODF static libraries using the same exception handling that you use in your part.  If you build multiple parts and use native exception handling in some and emulated exception handling in others then you will need to make sure that you build two sets of ODF static libraries and link each part against the correct static libraries.  We strongly recommend you make one choice and stick to it in all of your parts.
  165.  
  166.  
  167.  
  168. Using Native Exceptions
  169.  
  170. If you decide to use native exception handling, then you're free to use the ANSI syntax if you wish, or to use the ODF exception handling macros.  Using the macros will make it easier for you to switch back to ODF exception emulation should you ever change your mind, but otherwise there is no advantage to using the macros.  If you're committed to using native exceptions, we recommend you simply use the native syntax.
  171.  
  172. Note that to be compatible with ODF and OpenDoc you will probably find it best if you restrict yourself to catching and throwing the ODF exception class FW_XException, which is essentially just a wrapper for an integer error code.  This is because exceptions cannot be thrown out of SOM entry points, and ODF provides many SOM entry points.  At each such entry point, ODF must provide a try/catch block to catch all exceptions. Error information must be extracted from exceptions and inserted into the Environment parameter.  If you use the FW_XException class, this conversion is done for you automatically by ODF.  If you throw objects of other C++ exception classes (such as std::bad_alloc), and don't catch them elsewhere and turn them into FW_XException objects, ODF will be forced to catch them at each SOM entry point and return them as the OpenDoc error kODErrUndefined.  It might be useful to see how this is done.  The following is a snippet of code from FWODPart.cpp:
  173.  
  174. void FW_CODPart::Release(Environment *ev, FW_CPart* part)
  175. {
  176.     FW_TRY
  177.     {
  178.         part->Release(ev);
  179.     }
  180.     FW_CATCH_BEGIN
  181.     FW_CATCH_REFERENCE(FW_XException, exception)
  182.     {
  183.         FW_SetException(ev, exception);
  184.     }
  185.     FW_CATCH_EVERYTHING()
  186.     {
  187.         FW_SetEvError(ev, kODErrUndefined);
  188.     }
  189.     FW_CATCH_END
  190. }
  191.  
  192. This code, when compiled using native exceptions, will be compiled as if it were written like this:
  193.  
  194. void FW_CODPart::Release(Environment *ev, FW_CPart* part)
  195. {
  196.     try
  197.     {
  198.         part->Release(ev);
  199.     }
  200.     catch(FW_XException& exception)
  201.     {
  202.         FW_SetException(ev, exception);
  203.     }
  204.     catch(...)
  205.     {
  206.         FW_SetEvError(ev, kODErrUndefined);
  207.     }
  208. }
  209.  
  210. Note that ODF catches all exceptions.   The first catch handler catches all exceptions of type FW_XException.  This handler is able to extract a meaningful error code to insert into the Environment parameter.  The second handler catches all remaining exceptions.  Unfortunately, there is no way for the last resort handler to extract any meaningful error code, so it must use the error code kODErrUndefined. Note that ODF could use additional catch blocks for other kinds of exceptions, but each such catch block would add additional code but would only handle a small range of error conditions.  Each additional catch block would have to be replicated in every SOM entry point, so what may seem like a small amount of code per entry point can result in a significant increase in code size of a part. We believe that the overhead in additional code just isn't worth it.  This may be disappointing to developers who hoped to use full ANSI C++ exception handling but it's a fact of life dictated by the need to use a language-neutral interface such as SOM.
  211.  
  212.  
  213.  
  214. Using ODF Emulated Exceptions
  215.  
  216. Even if you decide to use ODF emulated exceptions, it's a good idea to be familar with ANSI C++ exception handling.  ODF emulated exception handling was designed to adhere as closely as possible to ANSI C++ exception handling. It wasn't possible to provide a 100% complete emulation, so you'll need to be aware of the limitations ODF emulation imposes.  However, even though ODF emulated exceptions is a subset of ANSI C++ exceptions, the subset is still a very rich feature set, and we won't attempt to document it all here.  If you understand ANSI C++ exception handling, and the limitations we document here, you should have little or no trouble using ODF emulated exceptions.
  217.  
  218.  
  219. Theory of Operation
  220.  
  221. It isn't necessary for you to understand how ODF emulated exception handling is implemented in order for you to use it,  but having a general understanding will make it easier to understand the limitations.
  222.  
  223. First, the FW_TRY macro uses the standard library function setjmp() to define a location to jump to.  The FW_THROW macro uses the standard library function longjmp()  to jump to the nearest FW_TRY block.  In order to know where the nearest try block is, the emulation system must maintain a linked list of state information for each FW_TRY block, and use a global variable that contains a pointer to the most recently entered try block that is still active.  When execution leaves a try block, the state information for that try block is removed from linked list, and the global variable is updated to point to the previously active try block.
  224.  
  225. When an exception is thrown via FW_THROW, the emulation system must unwind the stack to the context of the nearest try block.  The emulation system must  destroy active stack-based objects in the scope of the try block before it long jumps to the try block.  Since not all objects need to be destroyed when an exception is thrown, it's useful to make a distinction between objects that need destruction and those that don't.  We call objects that want automatic destruction AutoDestruct objects (or sometimes just "autodestruct"objects).  Note that AutoDestruct objects do not need to derive from any common base class (this is a change from prerelease versions of ODF Exception handling, which did require that AutoDestruct objects be derived from a special base class).
  226.  
  227. The only way the emulation system can know which objects are AutoDestruct objects is by requiring that those objects make their existence known to the emulation system.  AutoDestruct objects do this by calling FW_END_CONSTRUCTOR in their constructor(s), and FW_START_DESTRUCTOR in their destructor.  FW_END_CONSTRUCTOR pushes an entry onto a stack maintained within the emulation system.  FW_START_DESTRUCTOR removes the entry from the stack.  When an exception is thrown, the emulation system walks down the stack, destroying objects, until it has destroyed all objects in the scope of the try block.  It then calls longjmp() to return to the nearest catch block.
  228.  
  229. Note that ANSI C++ specifies that only fully constructed objects have their destructors invoked.   This means that it is necessary for objects to notify the exception system every time a constructor completes.  
  230.  
  231. Suppose class A is an AutoDestruct class, and class B is derived from A (so class B is also AutoDestruct).  If an exception is thrown from class B's constructor, then only A is fully constructed, so only A::~A() should be called.  The class B constructor should have the FW_END_CONSTRUCTOR call as the last line of code in the constructor.  If the exception is thrown before the FW_END_CONSTRUCTOR is executed, the exception emulation system will call the A::~A() destructor but will not call the B::~B() destructor.
  232.  
  233. The automatic destruction of stack-based objects is reasonably well-documented by most C++ reference books, and fairly easy to understand.  What is not as well-known is that the proposed ANSI C++  specification calls for the automatic destruction of objects allocated via new in the event of constructor failure.  Using the example above with classes A and B, suppose an instance of class B is created using the new operator.  If both the A::A() and B::B() constructors excecute completely, the exception handling system is not responsible for deleting the object.  However if an exception is thrown from the B::B() constructor, then the exception handling system is responsible for calling the A::~A() destructor, and for deleting the raw memory.
  234.  
  235. ODF exception emulation handles constructor failure for objects allocated via the new operator using the FW_NEW macro.  This macro creates a temporary stack-based object whose sole purpose is to delete the raw memory allocated in the event of an exception being thrown during construction.  The macro also arranges to notify the exception system when the object has been fully created.  During construction of the heap-based object, the FW_END_CONSTRUCTOR macros execute as normal.  When the exception system is notified that the heap-based object is fully constructed, it marks all of the entries created by the FW_END_CONSTRUCTOR as being already fulfilled.  If an exception is thrown, these entries will be skipped.
  236.  
  237. To complete the picture, all that remains is to describe how an exception object is thrown and caught.  It is relatively easy to make a class be throwable.  This is done via macros FW_DECLARE_EXCEPTION and FW_DEFINE_EXCEPTION.  These macros define functions that add RTTI information to the class, and define functions that make it possible for the exception system to safely copy an exception object even if the full type of the object isn't known.  Note that ODF exception objects are always thrown by value.  Throwing an exception object causes the object to be copied to a safe place, the longjmp() to occur, and then potentially another copy of the object to occur.  Since the object may be copied several times, it is important to be sure that the copies are destroyed when they are no longer in use.  The FW_DECLARE_EXCEPTION and FW_DEFINE_EXCEPTION macros generate functions that make it possible for the exception system to call the correct copy constructors and destructors.
  238.  
  239. Finally, exceptions are caught by type.  ODF exception emulation determines the type of the object using ODF RTTI emulation for this purpose.
  240.  
  241.  
  242. Limitations
  243.  
  244. 1) Cleanup of stack-based objects only occurs for classes that use the FW_DECLARE_AUTO, FW_DEFINE_AUTO, FW_END_CONSTRUCTOR, and FW_START_DESTRUCTOR macros.  If you remember the FW_DECLARE_AUTO macro but forget the FW_DEFINE_AUTO macro, you should get an error at link time.  If you use the FW_END_CONSTRUCTOR macro but omit the FW_START_DESTRUCTOR macro, you will get a debug message at runtime (from the debug build only).  If you omit both the FW_END_CONSTRUCTOR and FW_START_DESTRUCTOR macros, cleanup will not happen, and there is no way for the emulation system to detect the omission!
  245.  
  246. 2) Multiple inheritance with virtual base AutoDestruct objects is not supported.  However, multiple inheritance of non-virtual bases is supported.  For example:
  247.  
  248. class A { public: FW_DECLARE_AUTO(A) ... };
  249. class B { public: FW_DECLARE_AUTO(B) ... };
  250. class C : public A, public B { public: FW_DECLARE_AUTO(C) ... };
  251.  
  252. In the above example, three AutoDestruct classes are defined, with C deriving from both A and B.  This is fully supported by ODF emulated exceptions (in prereleases of ODF, it was not supported).
  253.  
  254. However, suppose there is a fourth class V, and A and B derived from it virtually:
  255.  
  256. class V { public:  ... };
  257. class A : public virtual V { public: FW_DECLARE_AUTO(A) ... };
  258. class B : public virtual V { public: FW_DECLARE_AUTO(B) ... };
  259. class C : public A, public B { public: FW_DECLARE_AUTO(C) ... };
  260.  
  261. ODF emulated exceptions does not support this case.  Note that it doesn't matter whether V is AutoDestruct or not; virtual base classes of AutoDestruct objects are not supported.
  262.  
  263. 3) Only one exception may be thrown at a time.  Consider the following try block:
  264.  
  265. FW_TRY
  266. {
  267.     ...
  268. }
  269. FW_CATCH_BEGIN
  270. FW_CATCH_EVERYTHING()
  271. {
  272.     FW_TRY
  273.     {
  274.         ...
  275.     }
  276.     FW_CATCH_BEGIN
  277.     FW_CATCH_EVERYTHING()
  278.     {
  279.         FW_THROW_SAME();    // OK
  280.         ...
  281.     }
  282.     FW_CATCH_END
  283.     FW_THROW_SAME();    // error, not supported
  284. }
  285. FW_CATCH_END
  286.  
  287. In order to be able to rethrow an exception, ODF emulated exception handling must keep a copy of the caught exception for the entire scope of the catch block.  ODF stores this exception in global data.  If another try/catch block is entered, this global space will become committed for use by the inner try/catch block.  When the inner catch block exits, the exception caught from the outer catch block is no longer remembered.  If you must nest a try/catch block inside a catch block, you can use the FW_THROW_SAME macro from within the inner catch block only.
  288.  
  289. 4) Exception objects can be no bigger than 128 bytes.
  290.  
  291. The global space used to safely copy exception objects is a buffer of 128 bytes.  There is no interface for increasing this storage at runtime.
  292.  
  293. 5) Only classes can be thrown as exceptions.
  294.  
  295. ODF RTTI is used to determine if a catch block matches an exception, so exception objects must have RTTI (the FW_DECLARE_EXCEPTION macro invokes the FW_DECLARE_CLASS macro, so it's not necessary to explicitly use the FW_DECLARE_CLASS macro).
  296.  
  297.  
  298. © 1993 - 1996 Apple Computer, Inc. All rights reserved.
  299. Apple, the Apple Logo, Macintosh, and OpenDoc are trademarks of Apple Computer, Inc., registered in the United States and other countries.